package KVService;

import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Random;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransportException;
import org.ini4j.Ini;
import org.ini4j.InvalidFileFormatException;

import Node.Node;
import Node.TMultiplexedProtocol;
import RMService.*;


/**
 * Class to exercise the functionality of a stand-alone client.
 * 
 * Used to run each of the experiments for the MP.
 *
 */
public class KVClientStandAlone {

	// Member variables
	private ArrayList<RMService.Client> clients;
	private Random randomGenerator;
	private static int KEYMAX = 1000000;
	
	// Enumeration for tests
	public enum Tests {
		INSERTGET,
		INSERTEMPTY,
		LOOKUPEMPTY,
		STORAGELOAD,
		INSERT,
		GET,
		DELETE
	}
	
	/**
	 * Constructor for the class.
	 * 
	 * @param hosts arraylist of hosts 
	 * @throws TTransportException
	 */
	public KVClientStandAlone (ArrayList<String[]> hosts) throws TTransportException {
		// Create client for each kv server
		clients = new ArrayList<RMService.Client> ();
		
		// Loop over the hosts
		for (String[] host : hosts) {
			
			// Create a new socket
			TSocket socket = new TSocket(host[0], Integer.parseInt(host[1]));
			
			// Open the socket
			try{
				socket.open();
			
				// Initialize clients protocol: JSON, compact, binary, etc.
				TBinaryProtocol protocol = new TBinaryProtocol(socket);
	
				// Register service (service name must match service name registered on server) 
				TMultiplexedProtocol mp = new TMultiplexedProtocol(protocol, "RMService");
				
				// Add the RMService client to the list of clients
				clients.add(new RMService.Client(mp));
				
				// Initialize the randomGenerator
				randomGenerator = new Random();
			}
			catch (TException e){
				System.out.println("Failed to connect to " + host);
			}
		}
	}
	
	/**
	 * Test: Plot the histogram of the lookup latency for a random key (when the system is empty).
	 * 
	 * @throws TException
	 */
	public void testLookupEmpty() throws TException {	
		// Member variables
		FileWriter fStream = null;
		BufferedWriter out = null;
				
		// Try to create the output data file
		try {
			fStream = new FileWriter("experiment1_experiment4_out.txt");
			out = new BufferedWriter(fStream);
			out.write("Insert_Index,Node_Issued_From,Key_Inserted,Elapsed_Time\n");
		}
		catch (Exception e) {
			System.err.println("Error opening file: " + e.getMessage());
		}
		
		// 1000 data points
		for (int i = 0; i < 1000; i++) {
			
			// Generate random key
			int key = randomGenerator.nextInt(KEYMAX);

			// Generate random node to issue insert from
			int index = randomGenerator.nextInt(clients.size());
			RMService.Client randClient = clients.get(index);
			
			// Perform lookup operation (lookup node, insert key value)
			long start = System.nanoTime();
			randClient.lookup(key);
			long elapsedTime = System.nanoTime() - start;
			System.out.println("Lookup " + i + ": " + elapsedTime);
			
			// Output csv-style data to file
			try {
				if (out != null) {
					out.write(i + "," + index + "," + key + "," + elapsedTime + "\n");
					out.flush();
				}
			}
			catch (Exception e) {
				System.err.println("Error writing: " + e.getMessage());
			}			
		}
	}
	
	/**
	 * Test: Plot the histogram of the insert latency for a random key (when the system is empty).
	 * 
	 * @throws TException
	 */
	public void testInsertEmpty() throws TException {
		// Member variables
		FileWriter fStream = null;
		BufferedWriter out = null;
		
		// Try to create the output data file
		try {
			fStream = new FileWriter("experiment2_out.txt");
			out = new BufferedWriter(fStream);
			out.write("Insert_Index,Node_Issued_From,Key_Inserted,Elapsed_Time\n");
		}
		catch (Exception e) {
			System.err.println("Error opening file: " + e.getMessage());
		}
		
		// 1000 data points
		for (int i = 0; i < 1000; i++) {
			// Generate random key
			int key = randomGenerator.nextInt(KEYMAX);

			// Generate random node to issue insert from
			int index = randomGenerator.nextInt(clients.size());
			RMService.Client randClient = clients.get(index);
			
			// Perform lookup operation (lookup node, insert key value)
			long start = System.nanoTime();
			int node = randClient.lookup(key);
			RMService.Client keyClient = clients.get(node);
			keyClient.insert(key, "TEST DATA");
			long elapsedTime = System.nanoTime() - start;
			System.out.println("Insert " + i + ": " + elapsedTime);
			
			// remove key to ensure data store remains empty for this test
			keyClient.deleteKey(key);
			
			// Output csv-style data to file
			try {
				if (out != null) {
					out.write(i + "," + index + "," + key + "," + elapsedTime + "\n");
					out.flush();
				}
			}
			catch (Exception e) {
				System.err.println("Error writing: " + e.getMessage());
			}	
		}
	}
	
	/**
	 * Test: Storage load (number of keys stored) across the 4 machines right after 
	 *       1000 random keys have been inserted
	 * 
	 * @throws TException
	 * @throws InterruptedException
	 */
	public void testStorageLoad() throws TException, InterruptedException {
		// Member variables
		FileWriter fStream = null;
		BufferedWriter out = null;
		
		// Try to create the output data file

		try {
			fStream = new FileWriter("experiment3_out.txt");
			out = new BufferedWriter(fStream);
			out.write("Insert_Index,Node_Issued_From,Key_Inserted\n");
		}
		catch (Exception e) {
			System.err.println("Error opening file: " + e.getMessage());
		}
		
		// Store 1000 key,value pairs
		for (int i = 0; i < 1000; i++) {
			// Generate random key
			int key = randomGenerator.nextInt(KEYMAX);

			// Generate random node to issue insert from
			int index = randomGenerator.nextInt(clients.size());
			RMService.Client randClient = clients.get(index);
			
			// Perform lookup operation (lookup node, insert key value)
			int node = randClient.lookup(key);
			RMService.Client keyClient = clients.get(node);
			keyClient.insert(key, "TEST DATA");
			System.out.println("Insert " + i);
			
			// Output csv-style data to file
			try {
				if (out != null) {
					out.write(i + "," + index + "," + key + "\n");
					out.flush();
				}
			}
			catch (Exception e) {
				System.err.println("Error writing: " + e.getMessage());
			}
		}
	}
	
	/**
	 * Average message overhead across the 4 machines for 1000 random lookups.
	 * 
	 * @throws TException
	 */
	public void testInsertFull() throws TException {		
		// 1000 data points
		for (int i = 0; i < 1000; i++) {
			// Generate random key
			int key = randomGenerator.nextInt(KEYMAX);

			// Generate random node to issue insert from
			int index = randomGenerator.nextInt(clients.size());
			RMService.Client randClient = clients.get(index);
			
			// Perform lookup operation (lookup node, insert key value)
			long start = System.nanoTime();
			int node = randClient.lookup(key);
			RMService.Client keyClient = clients.get(node);
			keyClient.insert(key, "TEST DATA");
			long elapsedTime = System.nanoTime() - start;
			System.out.println("Insert " + i + ": " + elapsedTime);
			
			// remove key to ensure data store remains empty for this test
			keyClient.deleteKey(key);
		}
	}
	
	public void testInsertGet() throws TException {
		
		// test data
		String testData = "TESTING";
		
		// initiate lookup from random node
		int index = randomGenerator.nextInt(clients.size());
		RMService.Client randClient = clients.get(index);
		
		int key = randomGenerator.nextInt(KEYMAX);
		int node = randClient.lookup(key);
		
		// insert key
		RMService.Client keyClient = clients.get(node);
		keyClient.insert(key, testData);
		
		// get key
		node = randClient.lookup(key);
		keyClient = clients.get(node);
		String ret = keyClient.get(key);
		
		System.out.println("Get returned same string: " + ret.equals(testData) + "return: " + ret);
		
		// delete key
		keyClient.deleteKey(key);		
	}
	
	public void insert(int node, int key, String value) throws TException{
		
		System.out.println("Insert ( " + key + ", " + value + ") at node " + node);
		RMService.Client client = clients.get(node);
		
		// get key
		node = client.lookup(key);
		RMService.Client keyClient = clients.get(node);
		System.out.println("Insert found key " + key + " at node " + node);
		keyClient.insert(key, value);
		System.out.println("Insert added (key,value) pair to node " + node);
	}
	
	public void get(int node, int key) throws TException{
		
		System.out.println("Get key " + key +" at node " + node);
		RMService.Client client = clients.get(node);
		
		// get key
		node = client.lookup(key);
		RMService.Client keyClient = clients.get(node);
		System.out.println("Get found key " + key + " at node " + node);
		String data = keyClient.get(key);
		System.out.println("Get retrieved data: "+ data);
	}
	
	public void delete(int node, int key) throws TException{
		
		System.out.println("Delete key " + key +" at node " + node);
		RMService.Client client = clients.get(node);
		
		// get key
		node = client.lookup(key);
		RMService.Client keyClient = clients.get(node);
		System.out.println("Delete found key " + key + " at node " + node);
		keyClient.deleteKey(key);
		System.out.println("Key Deleted");
	}
	
	/**
	 * Function to display the usage
	 */
	public static void DisplayUsage() {
	    System.out.println("\nKVClientStandAlone - Used to launch a kv client\n");
	    System.out.println("\tUsage: ./KVClientStandAlone <topology-ini> <test-name> <node> <key> <value>");
	}
	
	/**
	 * Main
	 * 
	 * @param args arguments
	 * @throws TException
	 * @throws InvalidFileFormatException
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws TException, InvalidFileFormatException, FileNotFoundException, IOException, InterruptedException {
		
		// Check for the correct number of arguments to be passed in
	    if (args.length < 2) {
	        // Print out an error message to the screen
	        System.out.println("Error: Incorrect number of input parameters!");
	        // Call function to display usage
	        KVClientStandAlone.DisplayUsage();
	        // Exit the program
	        System.exit(0);
	    }
	    
	    // Create an ini object for the topology information
	    Ini topologyIni = new Ini();
	    topologyIni.load(new FileReader(args[0]));
	    // Parse the addresses section from the topology ini file
        Ini.Section section = topologyIni.get("Nodes");
        String addresses = section.get("Addresses");
        ArrayList<String[]> hosts = Node.ParseAddresses(addresses);
        KVClientStandAlone testDriver = new KVClientStandAlone(hosts);
        
        // run test
        Tests test = Tests.valueOf(args[1].toUpperCase());
        
        // Switch over which test to run
        switch (test) {
	        case INSERTGET:
	            testDriver.testInsertGet();
	            break;
	        case INSERTEMPTY:
	            testDriver.testInsertEmpty();
	            break;
	        case LOOKUPEMPTY:
	            testDriver.testLookupEmpty();
	            break;
	        case STORAGELOAD:
	            testDriver.testStorageLoad();
	            break;
	        case INSERT:
	            testDriver.insert(Integer.parseInt(args[2]), Integer.parseInt(args[3]), args[4]);
	            break;
	        case GET:
	            testDriver.get(Integer.parseInt(args[2]), Integer.parseInt(args[3]));
	            break;
	        case DELETE:
	            testDriver.delete(Integer.parseInt(args[2]), Integer.parseInt(args[3]));
	            break;
	        default:
	        	System.out.println("Test " + args[1] + " does not exist");    	
        }
	}
}
